using Engine.Graphics.VertexStructures;
using SharpDX;
using SharpDX.Direct3D11;
using System;
using System.Collections.Generic;
using System.Text;

namespace Engine.Graphics
{
    public class DebugInfo : IRenderableObject
    {
        #region Private Fields

        private static StringBuilder infoMessages = new StringBuilder();

        private Color4 colorBlue = new Color4()
        {
            Alpha = 1.0f,
            Red = 0,
            Green = 0,
            Blue = 1
        };

        private Color4 colorGreen = new Color4()
        {
            Alpha = 1.0f,
            Red = 0,
            Green = 1,
            Blue = 0
        };

        private RenderText debugInfo;
        private Graphic engine;
        private Dictionary<string, TimesForRendering> infoRenderingTimes;
        private RenderText mouseCoordinatesOnScreen;
        private SharpDX.Direct3D11.Buffer rayVertexBuffer;
        private BufferDescription rayVertexBufferDescription;
        private CustomVertex.PositionColored[] rayVerts;
        private RenderText rtListOfRenderObjects;
        private StringBuilder sb = new StringBuilder();
        private VertexBufferBinding vertexBufferBinding;

        #endregion Private Fields

        #region Public Constructors

        public DebugInfo(Graphic engine)
        {
            this.engine = engine;
            //ray
            rayVerts = new CustomVertex.PositionColored[]
            {
               new CustomVertex.PositionColored(engine.VNear, new Color4(System.Drawing.Color.Red.ToArgb())),
               new CustomVertex.PositionColored(engine.VFar, new Color4(System.Drawing.Color.Blue.ToArgb()))
            };

            this.rayVertexBufferDescription = new BufferDescription()
            {
                BindFlags = BindFlags.VertexBuffer,
                CpuAccessFlags = CpuAccessFlags.Write,
                OptionFlags = ResourceOptionFlags.None,
                SizeInBytes = CustomVertex.PositionColored.SizeInBytes() * 2,
                StructureByteStride = CustomVertex.PositionColored.SizeInBytes(),
                Usage = ResourceUsage.Dynamic
            };

            lock (this.engine.Device.ImmediateContext)
            {
                rayVertexBuffer = SharpDX.Direct3D11.Buffer.Create(this.engine.Device, rayVerts, this.rayVertexBufferDescription);
                this.vertexBufferBinding = new VertexBufferBinding(this.rayVertexBuffer, CustomVertex.PositionColored.SizeInBytes(), 0);
            }

            // All Text Messages
            debugInfo = new RenderText(this.engine);
            debugInfo.SetTextOnScreen("Starting...", 10, 10, colorGreen);
            mouseCoordinatesOnScreen = new RenderText(this.engine);
            mouseCoordinatesOnScreen.SetTextOnScreen("MouseCoordinates...", 10, 60, colorBlue);
            this.rtListOfRenderObjects = new RenderText(this.engine);
            this.infoRenderingTimes = new Dictionary<string, TimesForRendering>();
        }

        #endregion Public Constructors

        #region Public Methods

        /// <summary>
        /// You can add a message which will displayed on screen. It will cleared after rendering.
        /// So you must add your info-message every renderupdate.
        /// </summary>
        /// <param name="message">The test of your message.</param>
        public static void AddInfoToScreen(string message)
        {
            infoMessages.AppendLine(message);
        }

        /// <summary>
        /// Records the rendertime for the given object name.
        /// </summary>
        /// <param name="objectName"></param>
        /// <param name="Rendertime">time in milliseconds</param>
        public void AddRenderTime(string objectName, int Rendertime)
        {
            TimesForRendering timesForRenderobjects;
            if (this.infoRenderingTimes.TryGetValue(objectName, out timesForRenderobjects))
            {
                timesForRenderobjects.AddRenderTime(Rendertime);
            }
            else
            {
                timesForRenderobjects = new TimesForRendering();
                timesForRenderobjects.AddRenderTime(Rendertime);
                this.infoRenderingTimes.Add(objectName, timesForRenderobjects);
            }
            sb.AppendLine(string.Format("{0} {1} ms ({2}) Max/Min {3}/{4}", objectName, Rendertime.ToString().PadLeft(3), timesForRenderobjects.averageRenderTime, timesForRenderobjects.maxRenderTime, timesForRenderobjects.minRenderTime));
        }

        void IRenderableObject.Dispose()
        {
            this.rayVertexBuffer.Dispose();
            this.rayVertexBuffer = null;
            this.debugInfo.Dispose();
            this.debugInfo = null;
            this.mouseCoordinatesOnScreen.Dispose();
            this.mouseCoordinatesOnScreen = null;
            this.rtListOfRenderObjects.Dispose();
            this.rtListOfRenderObjects = null;
        }

        void IRenderableObject.Render(int renderStep)
        {
            this.sb.Clear();
            RenderRay();
            this.rtListOfRenderObjects.Render(renderStep);
            ((IRenderableObject)debugInfo).Render(renderStep);
            ((IRenderableObject)mouseCoordinatesOnScreen).Render(renderStep);
        }

        public void Update()
        {
            Vector3 direction = UpdateRay();
            debugInfo.Message = rayVerts[0].Position.ToString() + "\n" + rayVerts[1].Position.ToString()
                + "\nDirection: " + direction.ToString();

            mouseCoordinatesOnScreen.Message = this.engine.MousePositionOnRenderControl.ToString();
        }

        public void UpdateListOfRenderObjects()
        {
            // Add info messages.
            sb.AppendLine();
            sb.Append(DebugInfo.infoMessages.ToString());
            DebugInfo.infoMessages.Clear();

            this.rtListOfRenderObjects.SetTextOnScreen(sb.ToString(), 10, 125, this.colorGreen);
        }

        #endregion Public Methods

        #region Private Methods

        private void RenderRay()
        {
            lock (this.engine.Device.ImmediateContext)
            {
                this.engine.Device.ImmediateContext.InputAssembler.PrimitiveTopology = SharpDX.Direct3D.PrimitiveTopology.LineList;
                this.engine.ShaderManager.SetShader(this.engine.ShaderManager.ColorShader);

                this.engine.Device.ImmediateContext.InputAssembler.SetVertexBuffers(0, this.vertexBufferBinding);
                this.engine.Device.ImmediateContext.Draw(2, 0);
            }
        }

        private Vector3 UpdateRay()
        {
            //Ray
            rayVerts[0].Position = this.engine.VFar;
            rayVerts[1].Position = this.engine.Camera.CameraTartget;

            DataStream vertexStream;
            lock (this.engine.Device.ImmediateContext)
            {
                engine.Device.ImmediateContext.MapSubresource(this.rayVertexBuffer, 0, MapMode.WriteDiscard, MapFlags.None, out vertexStream);
                vertexStream.WriteRange(rayVerts);
                engine.Device.ImmediateContext.UnmapSubresource(this.rayVertexBuffer, 0);
            }

            Vector3 direction = Vector3.Normalize(rayVerts[0].Position - rayVerts[1].Position);

            return direction;
        }

        #endregion Private Methods

        #region Private Classes

        private class TimesForRendering
        {
            #region Public Fields

            public int averageRenderTime = 0;
            public int maxRenderTime = 0;
            public int minRenderTime = int.MaxValue;

            #endregion Public Fields

            #region Private Fields

            private int completRenderTime = 0;
            private int newValuesCount = 0;

            #endregion Private Fields

            #region Public Methods

            public void AddRenderTime(int renderTime)
            {
                this.maxRenderTime = Math.Max(this.maxRenderTime, renderTime);
                this.minRenderTime = Math.Min(this.minRenderTime, renderTime);
                this.completRenderTime += renderTime;
                this.newValuesCount++;

                if (newValuesCount >= 50)
                {
                    averageRenderTime = this.completRenderTime / 50;
                    newValuesCount = 0;
                    this.completRenderTime = 0;
                }
            }

            #endregion Public Methods
        }

        #endregion Private Classes
    }
}